/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2009 - 2012 Luca Mingardi.
 *
 * This file is part of jeeObserver.
 *
 * JeeObserver is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * JeeObserver is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */
package jeeobserver.server;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import jeeobserver.ActionObserver;
import jeeobserver.server.ActionParameters.SearchElementsParameter;
import jeeobserver.server.ActionStatistics.Element;
import jeeobserver.server.ActionStatistics.TimeStatistics;
import jeeobserver.server.ActionStatistics.TotalStatistics;
import jeeobserver.server.DatabaseFile.DatabaseEntity;
import jeeobserver.server.DatabaseFileElements.DatabaseElementEntity;
import jeeobserver.server.DatabaseFileStatistics.DatabaseStatisticsEntity;
import jeeobserver.server.Statistics.DateStatistics;
import jeeobserver.server.Statistics.NumberStatistics;
import jeeobserver.server.Statistics.SamplingStatistics;
import jeeobserver.utilities.DatabaseUtilities;

class FileDatabaseHandlerAction {

	 private static final String ELEMENTS_FILE_NAME = "action_elements";

	 private static final String STATISTICS_FILE_NAME = "action_statistics";

	 private static final String PRINCIPAL_STATISTICS_FILE_NAME = "action_principal_statistics";

	 private String path;

	 private DatabaseFileElements<ElementEntity> fileElements;

	 private DatabaseFileStatistics fileStatistics;

	 private DatabaseFileStatistics filePrincipalStatistics;

	 private int samplingPeriod;

	 //Chached indexes
	 FileDatabaseHandlerAction(String path, int samplingPeriod) throws IOException {

			this.path = path;

			this.samplingPeriod = samplingPeriod;

			//Create files (if not exists)
			this.fileElements = new DatabaseFileElements<ElementEntity>(this.path + ELEMENTS_FILE_NAME, ElementEntity.class);
			this.fileStatistics = new DatabaseFileStatistics(this.path + STATISTICS_FILE_NAME, ActionStatisticsEntity.class, ActionStatisticsEntity.BYTES_SIZE, this.samplingPeriod);

			this.filePrincipalStatistics = new DatabaseFileStatistics(this.path + PRINCIPAL_STATISTICS_FILE_NAME, ActionPrincipalStatisticsEntity.class, ActionPrincipalStatisticsEntity.BYTES_SIZE, this.samplingPeriod);
	 }

	 synchronized void persistStatistics(Collection<ActionObserver> observers) throws IOException {

			if (observers.isEmpty()) {
				 return;
			}

			JeeObserverServerContext.logger.log(Level.FINER, "Persisting {0} Hard Disk statistics.", String.valueOf(observers.size()));

			Map<Integer, DatabaseStatisticsEntity> statisticsMap = new HashMap<Integer, DatabaseStatisticsEntity>();
			Map<Integer, DatabaseElementEntity> elementsMap = new HashMap<Integer, DatabaseElementEntity>();

			Map<Integer, DatabaseStatisticsEntity> principalStatisticsMap = new HashMap<Integer, DatabaseStatisticsEntity>();

			//Iterate and group all invocations
			for (final ActionObserver observer : observers) {

				 //Find statistics sampling date
				 final Date samplingDate = DatabaseUtilities.calculateStatisticTimestamp(observer.getDate(), this.samplingPeriod);

				 //Insert into cache all elements
				 ElementEntity elementEntity = new ElementEntity(observer.getContext(), observer.getProject(), observer.getCategory(), observer.getName());

				 if (!elementsMap.containsKey(elementEntity.getElement().getId())) {
						elementsMap.put(elementEntity.getElement().getId(), elementEntity);
				 }

				 //Insert into cache all statistics
				 DatabaseStatisticsEntity statisticsEntity = new ActionStatisticsEntity(elementEntity.getElement().getId(), samplingDate, observer);

				 int statisticsId = (statisticsEntity.getElementId() + "|" + statisticsEntity.getDate().getTime()).hashCode();

				 if (!statisticsMap.containsKey(statisticsId)) {
						statisticsMap.put(statisticsId, statisticsEntity);
				 } else {
						statisticsMap.get(statisticsId).add(statisticsEntity);
				 }

				 //Insert into cache all principals
				 if (observer.getPrincipal() != null) {

						//Insert into cache all principal statistics
						ActionPrincipalStatisticsEntity principalStatisticsEntity = new ActionPrincipalStatisticsEntity(elementEntity.getElement().getId(), samplingDate, observer);

						int principalStatisticsId = (principalStatisticsEntity.getElementId() + "|" + principalStatisticsEntity.getPrincipalId() + "|" + statisticsEntity.getDate().getTime()).hashCode();

						if (!principalStatisticsMap.containsKey(principalStatisticsId)) {
							 principalStatisticsMap.put(principalStatisticsId, principalStatisticsEntity);
						} else {
							 principalStatisticsMap.get(principalStatisticsId).add(principalStatisticsEntity);
						}
				 }
			}


			//Persist cached elements and statistics into Database
			this.fileElements.persist(elementsMap.values());
			this.fileStatistics.persist(statisticsMap.values());


			//Persist cached principals and statistics into Database
			this.filePrincipalStatistics.persist(principalStatisticsMap.values());
	 }

	 synchronized Collection<Element> searchElements(ActionParameters.SearchElementsParameter parameters) throws IOException {

			JeeObserverServerContext.logger.log(Level.FINE, "Searching Hard Disk elements. {0}", parameters.toString());

			final Collection<Element> resultList = new ArrayList<Element>();

			this.fileElements.reset();

			ElementEntity elementEntity;

			while ((elementEntity = (ElementEntity) this.fileElements.read()) != null) {
				 if (elementEntity.passFilter(parameters.getElementId(), parameters.getContext(), parameters.getProject(), parameters.getCategory(), parameters.getElement())) {
						resultList.add(elementEntity.getElement());
				 }
			}

			return resultList;
	 }

	 synchronized Collection<ActionStatistics> searchStatistics(ActionParameters.SearchStatisticsParameter parameters) throws IOException {

			JeeObserverServerContext.logger.log(Level.FINE, "Searching Hard Disk statistics. {0}", parameters.toString());

			//Check groupBy parameter
			if (parameters.getGroupBy() != RequestParameter.GROUP_BY_CONTEXT && parameters.getGroupBy() != RequestParameter.GROUP_BY_CONTEXT_PROJECT && parameters.getGroupBy() != RequestParameter.GROUP_BY_CONTEXT_PROJECT_ELEMENT) {
				 throw new IOException("GroupBy: " + parameters.getGroupBy() + " parameter not valid.");
			}

			//Search elements
			Collection<Element> elements = this.searchElements(new SearchElementsParameter(parameters.getCategory(), parameters.getElementId(), parameters.getContext(), parameters.getProject(), parameters.getElement()));

			//Group by
			Map<Integer, Element> elementsGroupedElements = new HashMap<Integer, Element>();
			Map<Integer, Integer> elementsGroupedIds = new HashMap<Integer, Integer>();

			for (Element element : elements) {

				 int elementGroupedId = 0;

				 if (parameters.getGroupBy() == RequestParameter.GROUP_BY_CONTEXT) {
						elementGroupedId = (element.getContext()).hashCode();
				 } else if (parameters.getGroupBy() == RequestParameter.GROUP_BY_CONTEXT_PROJECT) {
						elementGroupedId = (element.getContext() + "|" + element.getProject()).hashCode();
				 } else if (parameters.getGroupBy() == RequestParameter.GROUP_BY_CONTEXT_PROJECT_ELEMENT) {
						elementGroupedId = element.getId();
				 }

				 if (!elementsGroupedIds.containsKey(element.getId())) {
						elementsGroupedIds.put(element.getId(), elementGroupedId);
				 }

				 if (!elementsGroupedElements.containsKey(elementGroupedId)) {
						elementsGroupedElements.put(elementGroupedId, element);
				 }
			}


			long startTime = System.currentTimeMillis();


			//Search principals

			Map<Date, Map<Integer, Set<Integer>>> principalsPerPeriodMap = new HashMap<Date, Map<Integer, Set<Integer>>>();

			List<ActionPrincipalStatisticsEntity> principalsList = this.filePrincipalStatistics.search(elementsGroupedIds.keySet(), parameters.getDateFrom(), parameters.getDateTo());

			for (ActionPrincipalStatisticsEntity principalEntity : principalsList) {

				 //Get grouped id
				 int elementGroupedId = elementsGroupedIds.get(principalEntity.getElementId());

				 //Get statistics sampling period
				 Date samplingDate = DatabaseUtilities.calculateStatisticTimestamp(principalEntity.getDate(), parameters.getSamplingPeriod());

				 if (!principalsPerPeriodMap.containsKey(samplingDate)) {
						principalsPerPeriodMap.put(samplingDate, new HashMap<Integer, Set<Integer>>());
				 }

				 if (!principalsPerPeriodMap.get(samplingDate).containsKey(elementGroupedId)) {
						principalsPerPeriodMap.get(samplingDate).put(elementGroupedId, new HashSet<Integer>());
				 }

				 principalsPerPeriodMap.get(samplingDate).get(elementGroupedId).add(principalEntity.getPrincipalId());

			}

			//Search statistics

			Map<Date, Map<Integer, ActionStatistics>> statisticsPerPeriodMap = new HashMap<Date, Map<Integer, ActionStatistics>>();

			List<ActionStatisticsEntity> statisticsList = this.fileStatistics.search(elementsGroupedIds.keySet(), parameters.getDateFrom(), parameters.getDateTo());

			for (ActionStatisticsEntity statisticEntity : statisticsList) {

				 //Get grouped id
				 int elementGroupedId = elementsGroupedIds.get(statisticEntity.getElementId());

				 //Get statistics sampling period
				 Date samplingDate = DatabaseUtilities.calculateStatisticTimestamp(statisticEntity.getDate(), parameters.getSamplingPeriod());

				 ActionStatistics statistics = null;

				 Element element = elementsGroupedElements.get(elementGroupedId);

				 if (parameters.getSamplingPeriod() == RequestParameter.SAMPLING_PERIOD_NONE) {
						if (parameters.getGroupBy() == RequestParameter.GROUP_BY_CONTEXT) {
							 statistics = new TotalStatistics(statisticEntity, element.getContext(), this.samplingPeriod);
						} else if (parameters.getGroupBy() == RequestParameter.GROUP_BY_CONTEXT_PROJECT) {
							 statistics = new TotalStatistics(statisticEntity, element.getProject(), this.samplingPeriod);
						} else if (parameters.getGroupBy() == RequestParameter.GROUP_BY_CONTEXT_PROJECT_ELEMENT) {
							 statistics = new TotalStatistics(statisticEntity, element.getName(), this.samplingPeriod);
						}
				 } else {
						statistics = new TimeStatistics(samplingDate, statisticEntity);
				 }

				 if (!statisticsPerPeriodMap.containsKey(samplingDate)) {
						statisticsPerPeriodMap.put(samplingDate, new HashMap<Integer, ActionStatistics>());
				 }

				 if (statisticsPerPeriodMap.get(samplingDate).containsKey(elementGroupedId)) {
						statisticsPerPeriodMap.get(samplingDate).get(elementGroupedId).add(statistics);
				 } else {
						statisticsPerPeriodMap.get(samplingDate).put(elementGroupedId, statistics);
						//Insert also principals statistics once
						if (principalsPerPeriodMap.containsKey(samplingDate) && principalsPerPeriodMap.get(samplingDate).containsKey(elementGroupedId)) {
							 if (statistics instanceof TotalStatistics) {
									((TotalStatistics) statisticsPerPeriodMap.get(samplingDate).get(elementGroupedId)).addUserStatistics(statistics.getDate(), new NumberStatistics(principalsPerPeriodMap.get(samplingDate).get(elementGroupedId).size()), statistics.getCount());
							 } else {
									statisticsPerPeriodMap.get(samplingDate).get(elementGroupedId).addUserStatistics(new NumberStatistics(principalsPerPeriodMap.get(samplingDate).get(elementGroupedId).size()));
							 }
						}
				 }
			}


			long stopTime = System.currentTimeMillis();

			System.out.println("Time: " + (stopTime - startTime));

			final Collection<ActionStatistics> resultList = new ArrayList<ActionStatistics>();

			for (Date date : statisticsPerPeriodMap.keySet()) {
				 Map<Integer, ActionStatistics> statisticsMap = statisticsPerPeriodMap.get(date);

				 for (ActionStatistics statistics : statisticsMap.values()) {

						if (statistics instanceof ActionStatistics.TotalStatistics) {
							 ((ActionStatistics.TotalStatistics) statistics).calculateTrendStatistics();
						}

						resultList.add(statistics);
				 }
			}

			return resultList;
	 }

	 synchronized void deleteStatistics(ActionParameters.DeleteStatisticsParameter parameters) throws IOException {

			JeeObserverServerContext.logger.log(Level.FINE, "Deleting Hard Disk statistics. {0}", parameters.toString());

			//Filter elements
			Collection<Element> elements = this.searchElements(new SearchElementsParameter(parameters.getCategory(), parameters.getElementId(), parameters.getContext(), parameters.getProject(), parameters.getElement()));

			Set<Integer> elementsSet = new HashSet<Integer>();

			for (ActionStatistics.Element element : elements) {
				 elementsSet.add(element.getId());
			}

			//Delete statistics
			this.fileStatistics.delete(elementsSet, parameters.getDateFrom(), parameters.getDateTo());


			Set<Integer> elementsToNotDeleteSet = this.fileStatistics.getActiveElementIds();

			//Delete elements
			this.fileElements.reset();

			ElementEntity elementEntity;

			while ((elementEntity = (ElementEntity) this.fileElements.read()) != null) {

				 if (elementEntity.getStatus() != DatabaseElementEntity.STATUS_DELETED) {
						if (!elementsToNotDeleteSet.contains(elementEntity.getElement().getId())) {
							 elementEntity.setStatus(DatabaseElementEntity.STATUS_DELETED);
							 this.fileElements.writePervious(elementEntity);
						}
				 }
			}
	 }

	 synchronized void close() throws IOException {
			this.fileStatistics.close();
			this.fileElements.close();
	 }

	 //Action entities
	 private final static class ElementEntity extends DatabaseElementEntity implements DatabaseEntity {

			private static final long serialVersionUID = 40L;

			private Element element;

			ElementEntity(ByteBuffer buffer) {
				 super(buffer.get());

				 this.element = new Element(buffer.getInt(), DatabaseUtilities.getString(buffer, buffer.getInt()), DatabaseUtilities.getString(buffer, buffer.getInt()), buffer.getInt(), DatabaseUtilities.getString(buffer, buffer.getInt()));
			}

			ElementEntity(byte[] bytes) {
				 this(ByteBuffer.wrap(bytes));
			}

			ElementEntity(String context, String project, int category, String name) {

				 super(DatabaseElementEntity.STATUS_ACTIVE);
				 this.element = new Element(context, project, category, name);
			}

			@Override
			public byte[] getBytes() {
				 ByteBuffer buffer = ByteBuffer.allocate(this.getSize());

				 buffer.put(this.getStatus());

				 buffer.putInt(this.element.getId());

				 buffer.putInt(this.element.getContext().getBytes().length);
				 buffer.put(this.element.getContext().getBytes());

				 buffer.putInt(this.element.getProject().getBytes().length);
				 buffer.put(this.element.getProject().getBytes());

				 //buffer.putInt(this.element.getCategory().getBytes().length);
				 buffer.putInt(this.element.getCategory());

				 buffer.putInt(this.element.getName().getBytes().length);
				 buffer.put(this.element.getName().getBytes());

				 return buffer.array();
			}

			@Override
			public int getSize() {
				 return (1 + 4 + 4 + 4 + 4 + 4 + element.getContext().getBytes().length + element.getProject().getBytes().length + element.getName().getBytes().length);
			}

			public boolean passFilter(Integer id, String context, String project, Integer category, String name) {

				 if (this.getStatus() == DatabaseElementEntity.STATUS_DELETED) {
						return false;
				 } else if (id != null && id.intValue() != this.element.getId()) {
						return false;
				 } else if (context != null && !context.equals(this.element.getContext())) {
						return false;
				 } else if (project != null && !project.equals(this.element.getProject())) {
						return false;
				 } else if (category != null && !category.equals(this.element.getCategory())) {
						return false;
				 } else if (name != null && !this.element.getName().contains(name)) {
						return false;
				 }

				 return true;
			}

			@Override
			public Element getElement() {
				 return element;
			}
	 }

	 private final static class ActionStatisticsEntity extends ActionStatistics implements DatabaseStatisticsEntity {

			private static final long serialVersionUID = 40L;

			public static final int BYTES_SIZE = 105;

			private final int elementId;

			private transient byte status;

			ActionStatisticsEntity(ByteBuffer buffer) {
				 this(buffer.get(), buffer.getInt(), new Date(buffer.getLong()), buffer.getInt(), buffer.slice());
			}

			ActionStatisticsEntity(byte[] bytes) {
				 this(ByteBuffer.wrap(bytes));
			}

			ActionStatisticsEntity(byte status, int elementId, Date date, int count, ByteBuffer buffer) {

				 super(date, count,
								 FileDatabaseHandler.getDateStatistics(buffer),
								 FileDatabaseHandler.getNumberRangeStatistics(buffer),
								 FileDatabaseHandler.getNumberRangeStatistics(buffer),
								 FileDatabaseHandler.getNumberStatistics(buffer),
								 FileDatabaseHandler.getNumberStatistics(buffer),
								 new NumberStatistics(Double.NaN));

				 this.elementId = elementId;

				 this.status = status;
			}

			ActionStatisticsEntity(int elementId, Date date, ActionObserver coreElement) {
				 super(date, 1,
								 new DateStatistics(coreElement.getDate()),
								 new NumberRangeStatistics(coreElement.getCpuNanos()),
								 new NumberRangeStatistics(coreElement.getTimeNanos()),
								 new NumberStatistics(1),
								 new NumberStatistics((coreElement.getException() != null ? 1 : Double.NaN)),
								 new NumberStatistics(Double.NaN));

				 this.elementId = elementId;

				 this.status = DatabaseStatisticsEntity.STATUS_ACTIVE;
			}

			@Override
			public byte[] getBytes() {

				 ByteBuffer buffer = ByteBuffer.allocate(BYTES_SIZE);

				 buffer.put(this.status);

				 buffer.putInt(this.elementId);

				 FileDatabaseHandler.fillBuffer(buffer, this);

				 FileDatabaseHandler.fillBuffer(buffer, this.getDateStatistics());

				 FileDatabaseHandler.fillBuffer(buffer, this.getCpuNanosStatistics());
				 FileDatabaseHandler.fillBuffer(buffer, this.getTimeNanosStatistics());

				 FileDatabaseHandler.fillBuffer(buffer, this.getExecutionsStatistics());
				 FileDatabaseHandler.fillBuffer(buffer, this.getExceptionsStatistics());
				 FileDatabaseHandler.fillBuffer(buffer, this.getUsersStatistics());

				 return buffer.array();
			}

			@Override
			public int getSize() {
				 return BYTES_SIZE;
			}

			@Override
			public int getElementId() {
				 return elementId;
			}

			@Override
			public byte getStatus() {
				 return status;
			}

			@Override
			public void setStatus(byte status) {
				 this.status = status;
			}

			@Override
			public void add(DatabaseStatisticsEntity entity) {
				 super.add((ActionStatisticsEntity) entity);
			}
	 }

	 private final static class ActionPrincipalStatisticsEntity extends SamplingStatistics implements DatabaseStatisticsEntity {

			private static final long serialVersionUID = 40L;

			public static final int BYTES_SIZE = 21;

			private final int elementId;

			private final int principalId;

			private transient byte status;

			private final Set<Integer> principalSet;

			ActionPrincipalStatisticsEntity(ByteBuffer buffer) {
				 this(buffer.get(), buffer.getInt(), new Date(buffer.getLong()), buffer.getInt(), buffer.getInt());
			}

			ActionPrincipalStatisticsEntity(byte[] bytes) {
				 this(ByteBuffer.wrap(bytes));
			}

			ActionPrincipalStatisticsEntity(byte status, int elementId, Date date, int count, int principalId) {

				 super(date, count);

				 this.elementId = elementId;

				 this.status = status;

				 this.principalId = principalId;

				 this.principalSet = new HashSet<Integer>();
				 this.principalSet.add(this.principalId);
			}

			ActionPrincipalStatisticsEntity(int elementId, Date date, ActionObserver observer) {
				 super(date, 1);

				 this.elementId = elementId;

				 this.status = DatabaseStatisticsEntity.STATUS_ACTIVE;

				 this.principalId = observer.getPrincipal().hashCode();

				 this.principalSet = new HashSet<Integer>();
				 this.principalSet.add(this.principalId);
			}

			@Override
			public byte[] getBytes() {

				 ByteBuffer buffer = ByteBuffer.allocate(BYTES_SIZE);

				 buffer.put(this.status);

				 buffer.putInt(this.elementId);

				 FileDatabaseHandler.fillBuffer(buffer, this);

				 buffer.putInt(this.principalId);

				 return buffer.array();
			}

			@Override
			public int getSize() {
				 return BYTES_SIZE;
			}

			@Override
			public int getElementId() {
				 return elementId;
			}

			@Override
			public byte getStatus() {
				 return status;
			}

			@Override
			public void setStatus(byte status) {
				 this.status = status;
			}

			@Override
			public void add(DatabaseStatisticsEntity entity) {
				 //super.add(((SamplingStatistics) entity).getCount());
			}

			public int getPrincipalId() {
				 return principalId;
			}
	 }
}
